Concepte fundamentale de limbaje de programare

3. Blocurile de construcție ale programului C# #1

Tipurile descrise în articolul anterior din acest tur C# sunt construite folosind următoarele blocuri: membri, proprietăți, câmpuri, metode și evenimente, expresii, declarații.

3.1 Membri

Membrii unei clase sunt fie membri statici, fie membri de instanță. Membrii statici aparțin claselor, iar membrii instanțelor aparțin obiectelor (instanțe de clase). Următoarea listă oferă o imagine de ansamblu asupra tipurilor de membri pe care o clasă poate conține :

Constante: Valori constante asociate cu clasa
Câmpuri: Variabile care sunt asociate cu clasa
Metode: Acțiuni care pot fi efectuate de clasă
Proprietăți: Acțiuni asociate cu citirea și scrierea denumite proprietăți ale clasei
Indexori: Acțiuni asociate cu indexarea instanțelor clasei ca o matrice
Evenimente: Notificări care pot fi generate de clasă
Operatori: Operatori de conversie și de expresie acceptați de clasă
Constructori: Acțiuni necesare pentru a inițializa instanțe ale unei clasei sau ale clasei în sine
Finalizatori: Acțiunile efectuate înainte ca instanțe ale clasei să fie eliminate definitiv
Tipuri: Tipuri imbricate declarate de către clasă

3.2 Accesibilitate

Fiecare membru al unei clase are o accesibilitate asociată, care controlează regiunile textului programului care pot accesa membrul. Există șase forme posibile de accesibilitate. Modificatorii de acces sunt rezumați mai jos:

public: Accesul nu este limitat.
private: Accesul este limitat la clasa respectivă.
protected: Accesul este limitat la clasa respectivă sau clasele derivate din clasa respectivă.
internal: Accesul este limitat la ansamblul curent (.exe sau .dll).
protected internal: Accesul este limitat la clasa respectivă, clasele derivate din clasa respectivă sau clasele din același ansamblu.
private protected: Accesul este limitat la clasa respectivă sau clasele derivate din acest tip în cadrul aceluiași ansamblu.

3.3 Câmpuri

Un câmp este o variabilă care este asociată cu o clasă sau cu o instanță a unei clase. Un câmp declarat cu modificatorul static definește un câmp static. Un câmp static identifică exact o singură locație de stocare. Indiferent câte instanțe ale unei clase sunt create, există o singură copie a unui câmp static. Un câmp declarat fără modificatorul static definește un câmp de instanță. Fiecare instanță a unei clase conține o copie separată a tuturor câmpurilor de instanță ale acelei clase. În exemplul următor, fiecare instanță a clasei Color are o copie separată a câmpurilor de instanță R, G și B, dar există o singură copie a câmpurilor statice Negru, Alb, Roșu, Verde și Albastru:
public class Color
{
    public static readonly Color Black = new(0, 0, 0);
    public static readonly Color White = new(255, 255, 255);
    public static readonly Color Red = new(255, 0, 0);
    public static readonly Color Green = new(0, 255, 0);
    public static readonly Color Blue = new(0, 0, 255);
    
    public byte R;
    public byte G;
    public byte B;

    public Color(byte r, byte g, byte b)
    {
        R = r;
        G = g;
        B = b;
    }
}

După cum a fost demonstrat în exemplul anterior, câmpurile de tip read-only pot fi declarate cu un modificator numai de tip read-only. Atribuirea unui câmp de tip read-only poate avea loc numai ca parte a declarației câmpului sau într-un constructor din aceeași clasă.

3.4 Metode

O metodă este un membru care implementează un calcul sau o acțiune care poate fi efectuată de un obiect sau o clasă. Metodele statice sunt accesate prin intermediul clasei. Metodele de instanță sunt accesate prin instanțe ale clasei.

Metodele pot avea o listă de parametri, care reprezintă valori sau referințe variabile transmise metodei. Metodele au un tip de return, care specifică tipul valorii calculate și returnate de metodă. Tipul de return al unei metode este nul dacă nu returnează o valoare.

Ca și tipurile, metodele pot avea, de asemenea, un set de parametri de tip, pentru care argumentele de tip trebuie specificate atunci când metoda este apelată. Spre deosebire de tipuri, argumentele tip pot fi adesea deduse din argumentele unui apel de metodă și nu trebuie să fie date explicit.

Semnătura unei metode trebuie să fie unică în clasa în care este declarată metoda. Semnătura unei metode constă din numele metodei, numărul de parametri de tip și numărul, modificatorii și tipurile parametrilor acesteia. Semnătura unei metode nu include tipul de returnare.

Când corpul unei metode este o singură expresie, metoda poate fi definită folosind un format de expresie compact, așa cum se arată în exemplul următor:

public override string ToString() => "This is an object";

3.5 Parametri

Parametrii sunt utilizați pentru a transmite valori sau referințe variabile la metode. Parametrii unei metode își obțin valorile reale din argumentele care sunt specificate atunci când metoda este invocată. Există patru tipuri de parametri: parametri de valoare, parametri de referință, parametri de ieșire și vectori de parametri.

Un parametru de valoare este utilizat pentru transmiterea argumentelor de intrare. Un parametru de valoare corespunde unei variabile locale care își obține valoarea inițială din argumentul care a fost transmis pentru parametru. Modificările aduse unui parametru de valoare nu afectează argumentul care a fost transmis pentru parametru.

Parametrii valorii pot fi opționali, prin specificarea unei valori implicite, astfel încât argumentele corespunzătoare să poată fi omise.

Un parametru de referință este utilizat pentru transmiterea argumentelor prin referință. Argumentul transmis pentru un parametru de referință trebuie să fie o variabilă cu o valoare definită. În timpul execuției metodei, parametrul de referință reprezintă aceeași locație de stocare ca și variabila argument. Un parametru de referință este declarat cu modificatorul ref. Următorul exemplu arată utilizarea parametrilor ref.

static void Swap(ref int x, ref int y)
{
    int temp = x;
    x = y;
    y = temp;
}

public static void SwapExample()
{
    int i = 1, j = 2;
    Swap(ref i, ref j);
    Console.WriteLine($"{i} {j}");    // "2 1"
}

Un parametru de ieșire este utilizat pentru transmiterea argumentelor prin referință. Este similar cu un parametru de referință, cu excepția faptului că nu necesită să atribuiți în mod explicit o valoare argumentului furnizat de apelant. Un parametru de ieșire este declarat cu modificatorul out. Următorul exemplu arată utilizarea parametrilor out folosind sintaxa introdusă în C# 7.

static void Divide(int x, int y, out int result, out int remainder)
{
    result = x / y;
    remainder = x % y;
}

public static void OutUsage()
{
    Divide(10, 3, out int res, out int rem);
    Console.WriteLine($"{res} {rem}");	// "3 1"
}

O matrice de parametri permite ca un număr variabil de argumente să fie transmis unei metode. O matrice de parametri este declarată cu modificatorul params. Doar ultimul parametru al unei metode poate fi o matrice de parametri, iar tipul unei matrice de parametri trebuie să fie un tip de matrice unidimensională. Metodele Write și WriteLine ale clasei System.Console sunt exemple bune de utilizare a matricei de parametri. Sunt declarate după cum urmează.

public class Console
{
    public static void Write(string fmt, params object[] args) { }
    public static void WriteLine(string fmt, params object[] args) { }
    // ...
}

Într-o metodă care utilizează o matrice de parametri, matricea de parametri se comportă exact ca un parametru obișnuit al unui tip de matrice. Cu toate acestea, într-o invocare a unei metode cu o matrice de parametri, este posibil să se transmită fie un singur argument de tipul matricei de parametri, fie orice număr de argumente ale tipului de element al matricei de parametri. În cazul din urmă, o instanță de matrice este creată și inițializată automat cu argumentele date. De exemplu:

int x, y, z;
x = 3;
y = 4;
z = 5;
Console.WriteLine("x={0} y={1} z={2}", x, y, z);

este echivalent cu a scrie următoarele.

int x = 3, y = 4, z = 5;

string s = "x={0} y={1} z={2}";
object[] args = new object[3];
args[0] = x;
args[1] = y;
args[2] = z;
Console.WriteLine(s, args);

3.6 Corpul metodei și variabilele locale

Corpul unei metode specifică instrucțiunile de executat atunci când metoda este invocată.

Corpul metodei poate declara variabile care sunt specifice invocării metodei. Astfel de variabile sunt numite variabile locale. O declarație de variabilă locală specifică un nume de tip, un nume de variabilă și, eventual, o valoare inițială. Următorul exemplu declară o variabilă locală i cu o valoare inițială zero și o variabilă locală j fără valoare inițială.

class Squares
{
    public static void WriteSquares()
    {
        int i = 0;
        int j;
        while (i < 10)
        {
            j = i * i;
            Console.WriteLine($"{i} x {i} = {j}");
            i++;
        }
    }
}

C# necesită ca o variabilă locală să fie alocată definitiv înainte ca valoarea acesteia să poată fi obținută. De exemplu, dacă declarația anterioară a lui i nu include o valoare inițială, compilatorul va raporta o eroare pentru utilizările ulterioare ale lui i, deoarece i nu ar fi cu siguranță atribuit în acele zone din program.

O metodă poate folosi declarații de tip return pentru a returna controlul apelantului său. Într-o metodă care returnează void, instrucțiunile return nu pot specifica o expresie. Într-o metodă care returnează non-void, instrucțiunile return trebuie să includă o expresie care calculează valoarea returnată.

3.7 Metode statice și de instanță

O metodă declarată cu un modificator static este o metodă statică. O metodă statică nu funcționează pe o anumită instanță și poate accesa direct doar membri statici.

O metodă declarată fără un modificator static este o metodă de instanță. O metodă de instanță funcționează pe o anumită instanță și poate accesa atât membrii statici, cât și membrii instanței. Instanța pe care a fost invocată o metodă de instanță poate fi accesată în mod explicit astfel. Este eronat să ne referim aici într-o metodă statică.

Următoarea clasă Entity are atât membri statici, cât și membri de instanță.

class Entity
{
    static int s_nextSerialNo;
    int _serialNo;
    
    public Entity()
    {
        _serialNo = s_nextSerialNo++;
    }
    
    public int GetSerialNo()
    {
        return _serialNo;
    }
    
    public static int GetNextSerialNo()
    {
        return s_nextSerialNo;
    }
    
    public static void SetNextSerialNo(int value)
    {
        s_nextSerialNo = value;
    }
}

Fiecare instanță Entity conține un număr de serie (și probabil alte informații care nu sunt afișate aici). Constructorul Entity (care este ca o metodă de instanță) inițializează noua instanță cu următorul număr de serie disponibil. Deoarece constructorul este un membru al instanței, este permis să accesați atât câmpul de instanță _serialNo, cât și câmpul static s_nextSerialNo.

Metodele statice GetNextSerialNo și SetNextSerialNo pot accesa câmpul static s_nextSerialNo, dar ar fi o eroare să acceseze direct câmpul instanței _serialNo.

Următorul exemplu arată utilizarea clasei Entity.

Entity.SetNextSerialNo(1000);
Entity e1 = new();
Entity e2 = new();
Console.WriteLine(e1.GetSerialNo());          // Generează "1000"
Console.WriteLine(e2.GetSerialNo());          // Generează "1001"
Console.WriteLine(Entity.GetNextSerialNo());  // Generează "1002"

Metodele statice SetNextSerialNo și GetNextSerialNo sunt invocate pe clasă, în timp ce metoda instanței GetSerialNo este invocată pe instanțe ale clasei.

3.8 Metode virtuale, de suprascriere și abstracte

Utilizăm metode virtuale, de suprascriere și abstracte pentru a defini comportamentul pentru o ierarhie de tipuri de clasă. Deoarece o clasă poate deriva dintr-o clasă de bază, acele clase derivate ar putea avea nevoie să modifice comportamentul implementat în clasa de bază. O metodă virtuală este una declarată și implementată într-o clasă de bază în care orice clasă derivată poate oferi o implementare mai specifică. O metodă de suprascriere este o metodă implementată într-o clasă derivată care modifică comportamentul implementării clasei de bază. O metodă abstractă este o metodă declarată într-o clasă de bază care trebuie suprascrisă în toate clasele derivate. De fapt, metodele abstracte nu definesc o implementare în clasa de bază.

Apelurile de metodă la metodele de instanță se pot rezolva fie la implementări de clasă de bază, fie de clase derivate. Tipul unei variabile determină tipul acesteia în timpul compilării. Tipul în timp de compilare este tipul pe care compilatorul îl folosește pentru a-și determina membrii. Cu toate acestea, o variabilă poate fi atribuită unei instanțe de orice tip derivat din tipul său în timp de compilare. Tipul de rulare este tipul instanței efective la care se referă o variabilă.

Când este invocată o metodă virtuală, tipul de rulare al instanței pentru care are loc acea invocare determină implementarea efectivă a metodei de invocat. Într-o invocare a unei metode non-virtuale, tipul de compilare al instanței este factorul determinant.

O metodă virtuală poate fi suprascrisă într-o clasă derivată. Când o declarație de metodă de instanță include un modificator de suprascriere, metoda înlocuiește o metodă virtuală moștenită cu aceeași semnătură. O declarație de metodă virtuală introduce o nouă metodă. O declarație de metodă de înlocuire specializează o metodă virtuală moștenită existentă, oferind o nouă implementare a acelei metode.

O metodă abstractă este o metodă virtuală fără implementare. O metodă abstractă este declarată cu modificatorul abstract și este permisă numai într-o clasă abstractă. O metodă abstractă trebuie să fie suprascrisă în fiecare clasă derivată non-abstractă.

Următorul exemplu declară o clasă abstractă, Expression, care reprezintă un nod de arbore de expresie și trei clase derivate, Constant, VariableReference și Operation, care implementează noduri de arbore de expresie pentru constante, referințe variabile și operații aritmetice. (Acest exemplu este similar, dar nu are legătură cu tipurile de arbore de expresie).

public abstract class Expression
{
    public abstract double Evaluate(Dictionary vars);
}Assignments

public class Constant : Expression
{
    double _value;
    
    public Constant(double value)
    {
        _value = value;
    }
    
    public override double Evaluate(Dictionary vars)
    {
        return _value;
    }
}

public class VariableReference : Expression
{
    string _name;
    
    public VariableReference(string name)
    {
        _name = name;
    }
    
    public override double Evaluate(Dictionary vars)
    {
        object value = vars[_name] ?? throw new Exception($"Unknown variable: {_name}");
        return Convert.ToDouble(value);
    }
}

public class Operation : Expression
{
    Expression _left;
    char _op;
    Expression _right;
    
    public Operation(Expression left, char op, Expression right)
    {
        _left = left;
        _op = op;
        _right = right;
    }
    
    public override double Evaluate(Dictionary vars)
    {
        double x = _left.Evaluate(vars);
        double y = _right.Evaluate(vars);
        switch (_op)
        {
            case '+': return x + y;
            case '-': return x - y;
            case '*': return x * y;
            case '/': return x / y;
            
            default: throw new Exception("Unknown operator");
        }
    }
}

Cele patru clase anterioare pot fi folosite pentru a modela expresii aritmetice. De exemplu, folosind cazuri ale acestor clase, expresia x + 3 poate fi reprezentată după cum urmează.

Expression e = new Operation(
    new VariableReference("x"),
    '+',
    new Constant(3));

Metoda Evaluate a unei instanțe Expression este invocată pentru a evalua expresia dată și a produce o valoare dublă. Metoda ia un argument Dictionary care conține nume de variabile (ca chei ale intrărilor) și valori (ca valori ale intrărilor). Deoarece Evaluate este o metodă abstractă, clasele non-abstracte derivate din Expression trebuie să suprascrie Evaluate.

Implementarea Evaluate a unei constante returnează pur și simplu constanta stocată. Implementarea unei VariableReference caută numele variabilei în dicționar și returnează valoarea rezultată. Implementarea unei operații evaluează mai întâi operanzii din stânga și din dreapta (invocând recursiv metodele lor Evaluate) și apoi efectuează operațiile aritmetice date.on.

Următorul program folosește clasele Expression pentru a evalua expresia x * (y + 2) pentru diferite valori ale lui x și y.

Expression e = new Operation(
    new VariableReference("x"),
    '*',
    new Operation(
        new VariableReference("y"),
        '+',
        new Constant(2)
    )
);
Dictionary vars = new();
vars["x"] = 3;
vars["y"] = 5;
Console.WriteLine(e.Evaluate(vars)); // "21"
vars["x"] = 1.5;
vars["y"] = 9;
Console.WriteLine(e.Evaluate(vars)); // "16.5"

3.9 Sarcini

TODO

Bibliografie

[1] Microsoft Corporation. C# Documentation, https://docs.microsoft.com/en-us/dotnet/csharp/, 2022.